######################################################################
# Documentation and license
#
# See README file.

######################################################################
# Prologue

$VERBOSE = false 

require "jabber4r/jabber4r"
require "xmlrpc/parser"
require "xmlrpc/create"
require "xmlrpc/config"
require "xmlrpc/utils"
require "xmlrpc/server"

######################################################################
# Modifications to Jabber::Protocol::Iq

module Jabber::Protocol
  class Iq
    
    # Addition

    def Iq.gen_rpc(session, id, to, method, *args)
      iq = Iq.new(session, id)
      iq.type = "set"
      iq.xmlns = "jabber:iq:rpc"
      iq.to = to

      iq.data = XMLRPC::Create.new(XMLRPC::XMLWriter::Simple.new).methodCall(method, *args)
      return iq
    end

    # Patch
    
    def send(wait=false, &block)
      if wait
        iq = nil
        blockedThread = Thread.current
        @session.connection.send(self.to_s) do |je| 
          if je.element_tag == "iq" and je.attr_id == @id
            je.consume_element
            # LOCAL CHANGE: WAS iq = Iq.from_element(@session, je)
            iq = je
            # LOCAL ADDITION
            yield iq
            blockedThread.wakeup
          end
        end
        Thread.stop
        return iq
      else
        @session.connection.send(self.to_s, block) if @session
      end
    end
  end
end


######################################################################
# Modifications to XMLRPC::Create 

class XMLRPC::Create

  # Patch

  def methodResponse(is_ret, *params)

    if is_ret 
      resp = params.collect do |param|
        @writer.ele("param", conv2value(param))
      end
      
      resp = [@writer.ele("params", *resp)]
    else
      if params.size != 1 or params[0] === XMLRPC::FaultException 
        raise ArgumentError, "no valid fault-structure given"
      end
      resp = @writer.ele("fault", conv2value(params[0].to_h))
    end

    
    tree = @writer.document(
      # LOCAL CHANGE: COMMENTED OUT
      # @writer.pi("xml", 'version="1.0"'),
      @writer.ele("methodResponse", resp) 
      )

    @writer.document_to_str(tree) + "\n"
  end

  # Patch
  
  def methodCall(name, *params)
    name = name.to_s

    if name !~ /[a-zA-Z0-9_.:\/]+/
      raise ArgumentError, "Wrong XML-RPC method-name"
    end

    parameter = params.collect do |param|
      @writer.ele("param", conv2value(param))
    end

    tree = @writer.document(
      # LOCAL CHANGE: COMMENTED OUT        
      # @writer.pi("xml", 'version="1.0"'),
      @writer.ele("methodCall",   
        @writer.tag("methodName", name),
        @writer.ele("params", *parameter)    
        )
      )

    @writer.document_to_str(tree) + "\n"
  end
end

######################################################################
# Jabber RPC

module Jabber
  module RPC
    class Client

      include XMLRPC::ParserWriterChooseMixin

      # Create a client that will speak to the remote object
      # designated by remote_jid.
      #
      # Client must talk over an existing Jabber session (more than
      # one client can share the same session, though).

      def initialize(session, remote_jid)
        @session, @remote = session, remote_jid
        @parser = nil
      end

      # From xmlrpc/client

      def proxy(prefix, *args)
        Proxy.new(self, prefix, args, :call)
      end

      # From xmlrpc/client

      def proxy2(prefix, *args)
        Proxy.new(self, prefix, args, :call2)
      end 

      # Defined for convenience, since #proxy and #proxy2 won't work
      # well when we have one object for one remote agent (rather than
      # more objects for one remote server, as in XMLRPC over HTTP),
      # and the method is called by its name only instead of
      # object.method().

      def proxy3
        Proxy.new(self, "", [], :call, "")
      end

      def call2(method, *args)
        iq = Jabber::Protocol::Iq.gen_rpc(@session, nil, @remote, method, *args)
        response, jabber_error = nil, nil
        iq.send(true) do |reply|
          case reply.attr_type
          when "error"
            jabber_error = reply.error.to_s
          when "result"
            response = reply.query.methodResponse.to_s
          end
        end

        if jabber_error
          [false, jabber_error.to_s]
        else
          parser.parseMethodResponse(response)
        end
      end

      def call(method, *args)
        ok, param = call2(method, *args)
        if ok
          param
        else
          raise param
        end
      end
    end    

    class Proxy

      # From xmlrpc/client

      def initialize(client, prefix, args=[], meth = :call, delim = ".")
        @client = client
        @prefix = prefix + delim
        @args   = args 
        @meth   = meth
      end

      def method_missing(mid, *args)
        pre = @prefix + mid.to_s
        arg = @args + args
        @client.send(@meth, pre, *arg)
      end
    end

    # The server part of the conversation

    class BasicServer < XMLRPC::BasicServer
      def initialize(jid, password, class_delim = "", port = 5222)
        super(class_delim)

        if jid.kind_of?(String)
          @jid = Jabber::JID.new(jid)
        else
          @jid = jid
        end

        @password = password
        
        @session = Jabber::Session.new(@jid.host, port)

        @session.add_message_listener do |msg|
          methodname, *args = @human_input_processor.call(msg.body)
          success, result = call_method(methodname, msg.from, *args)
          # Better use an exception here, nil might be a valid value.
          if not result
            msg.reply.set_body("uhm?").send
          else
            msg.reply.set_body(@human_output_processor.call(methodname, result)).send
          end            
        end

        @session.iqHandlers["jabber:iq:rpc"] = proc do |iq|
          call = iq.data.element_children["methodCall"].to_s
          methodname, args = parser.parseMethodCall(call)
          reply = iq.reply
          reply.xmlns = "jabber:iq:rpc"
          reply.data = create.methodResponse(*call_method(methodname, iq.from, *args))
          reply.send
        end 
      end

      attr_accessor :human_input_processor, :human_output_processor

      def serve
        @session.authenticate(@jid.node, @password, @jid.resource) or
          raise "Authentication failed" 
        @session.request_roster
        @session.register_message_filter
        @session.register_presence_filter
        @session.register_iq_filter
        @session.announce_initial_presence
      end

      def shutdown
        @session.release
      end
    end
  end
end


if __FILE__ == $0
  require "test/unit"
  require "logger"
  log = Logger.new(STDERR)

  # Modify the following four constants with valid values to have
  # tests work on your system.
  #
  # Note: Jabber4r seems to have trouble logging in to jabber.org,
  # either choose a different server or install your own.

  AGENT_JID, AGENT_PWD = "bot@localhost/Test", "bot"
  CLIENT_JID, CLIENT_PWD = "bard@localhost/Test", "bard"

  log.info "Setting up agent"

  class TestAgent
    INTERFACE = XMLRPC::interface("") {
      meth "boolean alive?()"
      meth "string echo(string)"
    }

    def alive?(from)
      true
    end

    def echo(from, phrase)
      phrase
    end 
  end

  agent = TestAgent.new

  
  log.info "Setting up server that exposes the agent"
  server = Jabber::RPC::BasicServer.new(AGENT_JID, AGENT_PWD)
  server.add_handler(TestAgent::INTERFACE, agent)

  # Handling human input, i.e. messages sent with IM clients

  COMMAND_REGEX = /^\s*(\w+)\s*[-:]?\s*/i
  server.human_input_processor = proc do |msg|
    if msg =~ COMMAND_REGEX 
      method = Regexp.last_match[1].downcase
      arg = Regexp.last_match.post_match
      if arg.empty?
        [method]
      else
        [method, arg]
      end
    else
      nil
    end
  end
  server.human_output_processor = proc do |method_name, result|
    result.to_s
  end
          
  log.info "Running server"
  server.serve

  log.info "Forking to run tests in child process"
  fork do
    class TestBasicAgent < Test::Unit::TestCase
      CLIENT_SESSION = Jabber::Session.bind(CLIENT_JID, CLIENT_PWD)

      def test_call_alive_through_client
        client = Jabber::RPC::Client.new(CLIENT_SESSION, AGENT_JID)
        assert_equal true, client.call("alive?")
      end

      def test_call_alive_through_proxy
        proxy = Jabber::RPC::Client.new(CLIENT_SESSION, AGENT_JID).proxy3
        assert_equal true, proxy.alive?
      end

      def test_call_echo
        client = Jabber::RPC::Client.new(CLIENT_SESSION, AGENT_JID)
        assert_equal "hello there", client.call("echo", "hello there")
      end

      def test_error_on_unknown_method
        client = Jabber::RPC::Client.new(CLIENT_SESSION, AGENT_JID)
        assert_raise(XMLRPC::FaultException) do
          client.call("non_existing_method")
        end
      end

      def test_call_echo_as_human
        current_thread = Thread.current
        reply = nil

        lid = CLIENT_SESSION.add_message_listener do |msg|
          reply = msg.body
          current_thread.wakeup
        end

        msg = Jabber::Protocol::Message.new(AGENT_JID)
        msg.session = CLIENT_SESSION
        msg.body = "echo hello"
        msg.send

        Thread.stop

        assert_equal "hello", reply

        CLIENT_SESSION.delete_message_listener(lid)
      end
    end
  end

  Process.wait
  server.shutdown
end

